package org.kingtop.shiro.filters;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.SignatureException;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import org.apache.shiro.web.util.WebUtils;
import org.kingtop.action.ResultBean;
import org.kingtop.action.StatusCodeEnum;
import org.kingtop.cache.IRedisTemplate;
import org.kingtop.cache.app.UserCacheUtil;
import org.kingtop.model.IUserPO;
import org.kingtop.service.ILogBaseService;
import org.kingtop.shiro.exception.ShiroExceptionEnum;
import org.kingtop.shiro.exception.VerifyCodeException;
import org.kingtop.shiro.util.ShiroUtil;
import org.kingtop.shiro.util.TokenUtil;
import org.kingtop.util.WebRequestUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * 过滤器，可以用于登录验证，token验证，权限过滤
 *
 * @author 陈金波
 * @date 2018/2/25 10:32
 */
public class FormAndTokenAccessControlFilter extends AuthenticatingFilter {
    private static final Logger log = LoggerFactory.getLogger(FormAndTokenAccessControlFilter.class);

    /**
     * 默认的token的参数名称
     */
    public static final String DEFAULT_TOKEN_PARAM = "token";
    /**
     * 默认的token在header中的参数名称
     */
    public static final String DEFAULT_TOKEN_HEADER_PARAM = "Authorization";
    /**
     * 验证码默认的参数名称
     */
    public static final String DEFALUT_KAPTCHA_PARAM_NAME = "kaptchaCode";
    /**
     * 存在session中验证码的key
     */
    public static final String DEFAULT_KAPTCHA_SESSION_KEY = "KAPTCHA_SESSION_KEY";
    /**
     * 默认的错误消息key
     */
    public static final String DEFAULT_ERROR_KEY_ATTRIBUTE_NAME = "shiroLoginFailure";
    /**
     * 默认的用户名参数名称
     */
    public static final String DEFAULT_USERNAME_PARAM = "username";
    /**
     * 默认的密码参数名称
     */
    public static final String DEFAULT_PASSWORD_PARAM = "password";
    /**
     * 默认的记住登录状态的参数名称
     */
    public static final String DEFAULT_REMEMBER_ME_PARAM = "rememberMe";

    /**
     * 是否开启验证码支持
     */
    private boolean kaptchaEbabled = true;

    /**
     * 前台提交的验证码参数名
     */
    private String kaptchaParam = DEFALUT_KAPTCHA_PARAM_NAME;

    /**
     * 验证码在session中保存的的值对应的key
     */
    private String kaptchaSessionKey = DEFAULT_KAPTCHA_SESSION_KEY;

    /**
     * 用户名参数名称
     */
    private String usernameParam = DEFAULT_USERNAME_PARAM;
    /**
     * 密码参数名称
     */
    private String passwordParam = DEFAULT_PASSWORD_PARAM;
    /**
     * 记住登录状态的参数名称
     */
    private String rememberMeParam = DEFAULT_REMEMBER_ME_PARAM;

    /**
     * 错误消息的参数名称
     */
    private String failureKeyAttribute = DEFAULT_ERROR_KEY_ATTRIBUTE_NAME;

    /**
     * token的参数名称
     */
    private String tokenParam = DEFAULT_TOKEN_PARAM;

    /**
     * token在header中的参数名称
     */
    private String tokenHeaderParam = DEFAULT_TOKEN_HEADER_PARAM;

    private SessionDAO sessionDAO;

    @Autowired(required = false)
    private ILogBaseService logBaseService = null;

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        String username = getUsername(request);
        String password = getPassword(request);
        return createToken(username, password, request, response);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginRequest(request, response)) {
            if (isPost(request)) {
                return false;
            }
            return true;
        } else if (isTokenLogin(request)) {
            //判断是否为token登录请求,并进行验证
            try {
                Claims claims = getClaims(request);
                String sessionId = claims.getId();
                WebSessionKey webSessionKey = new WebSessionKey(sessionId, request, response);
                Session session = SecurityUtils.getSecurityManager().getSession(webSessionKey);
                Subject subject = (new Subject.Builder().session(session)).buildSubject();
                ThreadContext.bind(subject);
                return true;
            } catch (AuthenticationException e) {
                setFailureAttribute(request, e);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return false;
        } else {
            Subject subject = getSubject(request, response);
            // If principal is not null, then the user is known and should be allowed access.
            return subject.getPrincipal() != null;
        }
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            //判断是否post请求
            if (isPost(request)) {
                if (log.isTraceEnabled()) {
                    log.trace("Login submission detected.  Attempting to execute login.");
                }
                //如果进行验证码验证，且验证不通过
                if (this.kaptchaEbabled && !kaptchaCodeValidate(request)) {
                    setFailureAttribute(request, new VerifyCodeException());
                    return true;
                }
                return executeLogin(request, response);
            } else {
                if (log.isTraceEnabled()) {
                    log.trace("Login page view.");
                }
                //allow them to see the login page ;)
                return true;
            }
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Attempting to access a path which requires authentication.  Forwarding to the " +
                        "Authentication url [" + getLoginUrl() + "]");
            }

            saveRequestAndRedirectToLogin(request, response);
            return false;
        }
    }

    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        IUserPO user = UserCacheUtil.get(subject.getPrincipal().toString());
        StackTraceElement[] temp = Thread.currentThread().getStackTrace();
        StackTraceElement a = (StackTraceElement) temp[2];
        // 方法名称
        String methodName = a.getMethodName();
        String className = this.getClass().getName();
        if (this.logBaseService != null) {
            this.logBaseService.addLog(user.getId(), "login", "用户登录[" + user.getNickname() + "]",
                    "用户登录[" + user.getNickname() + "]", WebRequestUtil.getRemoteIp((HttpServletRequest) request), className, methodName);
        }

        issueSuccessRedirect(request, response);
        //we handled the success redirect directly, prevent the chain from continuing:
        return false;
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        setFailureAttribute(request, e);
        //login failed, let request continue back to the login page:
        return true;
    }

    /**
     * (non-Javadoc)
     *
     * @see org.apache.shiro.web.filter.AccessControlFilter#redirectToLogin(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
     */
    @Override
    protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
        //判断是否为post请求，如果是，则返回json格式数据
        if (isPost(request)) {
            ResultBean<String> result;
            Object exceptionClassName = request.getAttribute(getFailureKeyAttribute());
            String error = exceptionClassName == null ? null : ShiroExceptionEnum.getByClassName(exceptionClassName.toString()).orElse(ShiroExceptionEnum.DEFAULT_EXCEPTION).getMessage();
            if (error != null) {
                result = new ResultBean<>(StatusCodeEnum.UNAUTHORIZED.getCode(), getLoginUrl(), error);
            } else {
                result = new ResultBean<String>(StatusCodeEnum.UNAUTHORIZED, getLoginUrl());
            }
            ShiroUtil.writeUnauthorityMessage(result, response);
        } else {
            String loginUrl = getLoginUrl();
            WebUtils.issueRedirect(request, response, loginUrl);
        }
    }

    /**
     * 判断是否为post请求
     *
     * @param request 请求
     * @return true：是，false：不是
     */
    protected boolean isPost(ServletRequest request) {
        return (request instanceof HttpServletRequest) && WebUtils.toHttp(request).getMethod().equalsIgnoreCase(POST_METHOD);
    }

    /**
     * 从参数或者header中获取token
     *
     * @param request 请求
     * @return token
     * @Author 陈金波
     * @Date 2018/2/10 20:29
     */
    private String getToken(ServletRequest request) {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        String token = httpServletRequest.getHeader(getTokenHeaderParam());
        if (StringUtils.isBlank(token)) {
            token = WebUtils.getCleanParam(request, getTokenParam());
        }
        return token;
    }

    /**
     * 判断是否为token登录
     *
     * @param request 请求
     * @return true：是token，false：不是token
     * @Author 陈金波
     * @Date 2018/2/25 11:01
     */
    private boolean isTokenLogin(ServletRequest request) {
        return StringUtils.isNotBlank(getToken(request));
    }

    /**
     * 进行token验证,如果正确则返回，否则会抛出异常<br>
     * ExpiredJwtException:超期
     * SignatureException:解析错误
     *
     * @param request 请求，如果验证失败，抛出异常
     * @return true验证成功
     * @Author 陈金波
     * @Date 2018/2/25 11:16
     */
    protected Claims getClaims(ServletRequest request) {
        String token = getToken(request);
        try {
            return TokenUtil.getClaims(token);
        } catch (SignatureException e) {
            throw new AuthenticationException(e);
        } catch (ExpiredJwtException e) {
            throw new ExpiredCredentialsException(e);
        } catch (Exception e) {
            throw e;
        }
    }

    /**
     * 验证验证码是否正确
     *
     * @param request 请求
     * @return true验证通过，false验证不通过
     */
    protected boolean kaptchaCodeValidate(ServletRequest request) {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        String validate = httpServletRequest.getParameter(this.kaptchaParam);
        String validateCode = (String) httpServletRequest.getSession().getAttribute(this.kaptchaSessionKey);
        if (StringUtils.isNotBlank(validate) && StringUtils.isNotBlank(validateCode) && StringUtils.equals(validate, validateCode)) {
            return true;
        }
        return false;
    }

    @Override
    protected boolean isRememberMe(ServletRequest request) {
        return WebUtils.isTrue(request, getRememberMeParam());
    }

    /**
     * 保存错误消息，参考自：FormAuthenticationFilter
     *
     * @param request 请求
     * @param ae      异常
     */
    protected void setFailureAttribute(ServletRequest request, AuthenticationException ae) {
        String className = ae.getClass().getName();
        request.setAttribute(getFailureKeyAttribute(), className);
    }

    /**
     * 获取用户名的参数值
     *
     * @param request 请求
     * @return 用户名
     */
    protected String getUsername(ServletRequest request) {
        return WebUtils.getCleanParam(request, getUsernameParam());
    }

    /**
     * 获取密码的参数值
     *
     * @param request 密码
     * @return 密码
     */
    protected String getPassword(ServletRequest request) {
        return WebUtils.getCleanParam(request, getPasswordParam());
    }

    public String getTokenParam() {
        return tokenParam;
    }

    public void setTokenParam(String tokenParam) {
        this.tokenParam = tokenParam;
    }

    public String getTokenHeaderParam() {
        return tokenHeaderParam;
    }

    public void setTokenHeaderParam(String tokenHeaderParam) {
        this.tokenHeaderParam = tokenHeaderParam;
    }

    public boolean isKaptchaEbabled() {
        return kaptchaEbabled;
    }

    public void setKaptchaEbabled(boolean kaptchaEbabled) {
        this.kaptchaEbabled = kaptchaEbabled;
    }

    public String getKaptchaParam() {
        return kaptchaParam;
    }

    public void setKaptchaParam(String kaptchaParam) {
        this.kaptchaParam = kaptchaParam;
    }

    public String getKaptchaSessionKey() {
        return kaptchaSessionKey;
    }

    public void setKaptchaSessionKey(String kaptchaSessionKey) {
        this.kaptchaSessionKey = kaptchaSessionKey;
    }

    public String getUsernameParam() {
        return usernameParam;
    }

    public void setUsernameParam(String usernameParam) {
        this.usernameParam = usernameParam;
    }

    public String getPasswordParam() {
        return passwordParam;
    }

    public void setPasswordParam(String passwordParam) {
        this.passwordParam = passwordParam;
    }

    public String getRememberMeParam() {
        return rememberMeParam;
    }

    public void setRememberMeParam(String rememberMeParam) {
        this.rememberMeParam = rememberMeParam;
    }

    public String getFailureKeyAttribute() {
        return failureKeyAttribute;
    }

    public void setFailureKeyAttribute(String failureKeyAttribute) {
        this.failureKeyAttribute = failureKeyAttribute;
    }

    public SessionDAO getSessionDAO() {
        return sessionDAO;
    }

    public void setSessionDAO(SessionDAO sessionDAO) {
        this.sessionDAO = sessionDAO;
    }

}
